/*
 * Copyright 2009 Michal Trzcinka
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed
 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
 * CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */

YAHOO.namespace('TRZCINKA.VCARD');
YAHOO.namespace('TRZCINKA.VCARD.MODEL');

(function() {

  var CustomEvent = YAHOO.util.CustomEvent,
  Constants = YAHOO.TRZCINKA.VCARD.Constants,
  LOGGER = new YAHOO.widget.LogWriter('model.js'),
  JSON = YAHOO.lang.JSON,
  Config = YAHOO.TRZCINKA.VCARD.Config,
  Connect = YAHOO.util.Connect;

  /**
   * A convenient function that takes location as an argument and returns
   * a proper search function. Used as an argument for Array.find and Array.findAll
   * functions.
   * @param location object containing row and column properties.
   */
  YAHOO.TRZCINKA.VCARD.MODEL.byLocation = function(location) {
    return function(item) {
      return item.location.equals(location);
    };
  };

  /**
   * A comparison function used as an argument to Array.max function.
   */
  YAHOO.TRZCINKA.VCARD.MODEL.byOrder = function(a, b) {
    return a.order - b.order;
  };

  var byLocation = YAHOO.TRZCINKA.VCARD.MODEL.byLocation;
  var byOrder = YAHOO.TRZCINKA.VCARD.MODEL.byOrder;

  /**
   * GridModel instance holds information about grid data, enables grid manipulation and notifies
   * its listeners about grid data changes. 
   */
  YAHOO.TRZCINKA.VCARD.GridModel = function(accountType) {
    LOGGER.log('instantiating GridModel', 'debug');
    this.gridData = null;
    this._mode = Constants.MODE_NORMAL;
    this.accountType = accountType;

    /**
     * YUI custom event that is fired whenever grid model changes.
     */
    this.gridModelEvent = new CustomEvent(Constants.VCARD_EVENT, this);

    this.messageEvent = new CustomEvent(Constants.MESSAGE_EVENT, this);

    YAHOO.lang.later(Config.POLL_INTERVAL, this, '_poll', null, true);
  }

  YAHOO.TRZCINKA.VCARD.GridModel.prototype._poll = function() {

    var callback = {
      success:
      function(o) {
        //LOGGER.log('Poll success, code:' + o.status);
        if (o.status == 200) {
          LOGGER.log('responseText is ' + o.responseText);
          var obj = JSON.parse(o.responseText);
          var that = this;
          obj.data.each(function(event) {
            that._handleEvent(event.data);
            that.gridModelEvent.fire(event.data);
          });
        }
      },
      failure:
      function(o){
        LOGGER.log('Error while polling!: ' + o.statusText, 'error');
      },
      scope: this
    };

    Connect.initHeader('Content-type', 'application/json', 'true');
    Connect.asyncRequest('GET', '/poll/index?grid_id=' + this.getGridId(), callback, null);
  }

  YAHOO.TRZCINKA.VCARD.GridModel.prototype._handleEvent = function(event) {
    var data = event[1];
    var type = event[0];

    switch(type) {
      case Constants.ITEM_SAVED_EVENT:
        var item = data;
        if (this.gridData.items == undefined) {
          this.gridData.items = [item];
        } else {
          this.gridData.items.push(item);
        }
        break;

      case Constants.ITEM_REMOVED_EVENT:
        item = this.getTopItem(data.location);
        this.gridData.items.remove(item);
        break;

      case Constants.ITEM_UPDATED_EVENT:
        item = this.getTopItem(data.location);
        if (data.backgroundColor) {
          item.backgroundColor = data.backgroundColor;
        }
        if (data.foregroundColor) {
          item.foregroundColor = data.foregroundColor;  
        }
        if (data.text) {
          item.text = data.text;
        }
        if (typeof data.isHighlighted != undefined) {
          item.isHighlighted = data.isHighlighted;
        }
        break;

      case Constants.ITEM_MOVED_EVENT:
        item = this.getTopItem(data.oldLocation);
        item.location = data.newLocation;
        item.order = data.order;
        break;

      case Constants.STACK_MOVED_EVENT:
        var stack = this.getItems(data.oldLocation);
        if (this.itemExists(data.newLocation)) {
          stack.sort(byOrder);
          var topOrder = this.getTopItem(data.newLocation).order;
          stack.each(function (item) {
            item.location = data.newLocation;
            item.order = topOrder++;
          });
        }else {
          stack.each(function (item) {
            item.location = data.newLocation;
          });
        }
        break;

      case Constants.TOP_ITEM_SET_EVENT:
        stack = this.gridData.items.findAll(byLocation(data.location)).sort(byOrder);
        var removed = stack.remove(function(item) {
          return item.order == data.order;
        });
        stack.push(removed);
        for (var i = 0; i < stack.length; i++) {
          stack[i].order = i + 1;
        }
        break;

      case Constants.ITEMS_LINKED_EVENT:
        this.gridData.links.push({
          from: data.from,
          to: data.to
        });
        break;

      case Constants.ITEMS_UNLINKED_EVENT:
        this.gridData.links.remove(function(link) {
          return link.from.equals(data.location1) && link.to.equals(data.location2) ||
          link.from.equals(data.location2) && link.to.equals(data.location1);
        });
        break;

      case Constants.DIMENSIONS_CHANGED_EVENT:
        this.messageEvent.fire(I18n.t('editor.messages.wait_refreshing'));
        break;
    }
  }

  YAHOO.TRZCINKA.VCARD.GridModel.prototype._defaultCallback =  {
    failure:
    function(o){
      LOGGER.log('Error: ' + o.statusText, 'error');
    }
  }

  YAHOO.TRZCINKA.VCARD.GridModel.prototype._prepareConnect = function() {
    Connect.setDefaultPostHeader(false);
    Connect.initHeader('Content-type', 'application/json', 'true');
  }

  /**
     * Returns an item that is on top at given location.
     */
  YAHOO.TRZCINKA.VCARD.GridModel.prototype.getTopItem = function(location) {
    LOGGER.log('getting top item at location ' + JSON.stringify(location), 'debug');
    return this.gridData.items.findAll(byLocation(location)).max(byOrder);
  };

  /**
     * Initializes the grid with given grid data.
     */
  YAHOO.TRZCINKA.VCARD.GridModel.prototype.loadGrid = function(gridDataParam) {
    LOGGER.log('GridModel.loadGrid called with ' + gridDataParam, 'debug');
    this.gridData = gridDataParam;
    this.gridModelEvent.fire([Constants.MODEL_LOADED_EVENT, this.gridData]);
  };

  /**
     * Returns grid id.
     */
  YAHOO.TRZCINKA.VCARD.GridModel.prototype.getGridId = function() {
    return this.gridData.id;
  };

  /**
     * Checks whether an item exists at given location.
     */
  YAHOO.TRZCINKA.VCARD.GridModel.prototype.itemExists = function(location) {
    LOGGER.log('checking if item ' + JSON.stringify(location) + ' exists', 'debug');
    if (this.gridData.items == undefined) {
      return false;
    }
    for (var i = 0; i < this.gridData.items.length; i++) {
      var item = this.gridData.items[i];
      if (item.location.equals(location)) {
        return true;
      }
    }
    return false;
  };

  YAHOO.TRZCINKA.VCARD.GridModel.prototype.saveItem = function(location, text) {
    LOGGER.log('saving item at ' + JSON.stringify(location) + '...', 'debug');
    var item = {
      text: text,
      backgroundColor: Config.DEFAULT_BACKGROUND_COLOR,
      foregroundColor: Config.DEFAULT_FOREGROUND_COLOR,
      order: 1,
      location: location
    };
    
    this._doSaveItem(item);
  };

  YAHOO.TRZCINKA.VCARD.GridModel.prototype._doSaveItem = function(item) {
    var event = [Constants.ITEM_SAVED_EVENT, item];
    this._handleEvent(event);
    this.gridModelEvent.fire(event);

    var object = {
      gridId: this.getGridId(),
      item: item
    }

    this._prepareConnect();
    Connect.asyncRequest('POST', '/items/save', this._defaultCallback, JSON.stringify({
      data: object
    }));
  }

  /**
     * Sets text of top item at given location.
     */
  YAHOO.TRZCINKA.VCARD.GridModel.prototype.updateItem = function(location, data) {
    LOGGER.log('updating item at ' + JSON.stringify(location) + ' with ' + JSON.stringify(data), 'debug');
    var item = this.getTopItem(location);

    data.location = location;
    var event = [Constants.ITEM_UPDATED_EVENT, data];
    this._handleEvent(event);
    this.gridModelEvent.fire(event);

    this._prepareConnect();
    Connect.asyncRequest('POST', '/items/update', this._defaultCallback, JSON.stringify({
      data: {
        gridId: this.getGridId(),
        item: item
      }
    }));
  };

  YAHOO.TRZCINKA.VCARD.GridModel.prototype.startDuplication = function(location) {
    this._itemToDuplicate = this.getTopItem(location);
    this._mode = Constants.MODE_DUPLICATING;
    this.messageEvent.fire(I18n.t('editor.messages.click_duplicate'));
  }

  YAHOO.TRZCINKA.VCARD.GridModel.prototype.duplicateItem = function(location) {
    var item = this._itemToDuplicate.clone();
    item.location = location;
    this._doSaveItem(item);
    this._mode = Constants.MODE_NORMAL;
    this._itemToDuplicate = null;
    this.messageEvent.fire('');
  }

  /**
    * Moves top item from oldLocation to the top at newLocation.
    */
  YAHOO.TRZCINKA.VCARD.GridModel.prototype.moveItem = function(oldLocation, newLocation, order) {
    LOGGER.log('moving item from ' + JSON.stringify(oldLocation) + ' to ' + JSON.stringify(newLocation), 'debug');

    //refreshing links
    var that = this;
    var linkFromLocations = this.gridData.links.findAll(function (link) {
      return link.from.equals(oldLocation);
    });

    linkFromLocations.each(function(linkLocation) {
      that.unlinkItem(oldLocation, linkLocation.to);
      that.linkItem(newLocation, linkLocation.to);
    });

    var linkToLocations = this.gridData.links.findAll(function (link) {
      return link.to.equals(oldLocation);
    });

    linkToLocations.each(function(linkLocation) {
      that.unlinkItem(linkLocation.from, oldLocation);
      that.linkItem(linkLocation.from, newLocation);
    });

    var data = {
      oldLocation: oldLocation,
      newLocation: newLocation,
      order: order,
      gridId: this.getGridId()
    };

    var event = [Constants.ITEM_MOVED_EVENT, data];
    this._handleEvent(event);
    this.gridModelEvent.fire(event);

    this._prepareConnect();
    Connect.asyncRequest('POST', '/items/move_item', this._defaultCallback, JSON.stringify({
      data: data
    }));
  }

  /**
     * Moves stack of items from oldLocation to newLocation.
     */
  YAHOO.TRZCINKA.VCARD.GridModel.prototype.moveStack = function(oldLocation, newLocation) {
    LOGGER.log('moving stack from ' + JSON.stringify(oldLocation) + ' to ' + JSON.stringify(newLocation), 'debug');
    var linkLocations = this.getLinkLocations(oldLocation);
    var that = this;
    linkLocations.each(function(linkLocation) {
      that.unlinkItem(oldLocation, linkLocation);
      that.linkItem(newLocation, linkLocation);
    });
    
    var data = {
      oldLocation: oldLocation,
      newLocation: newLocation,
      gridId: this.getGridId()
    };
    var event = [Constants.STACK_MOVED_EVENT, data];
    this._handleEvent(event);
    this.gridModelEvent.fire(event);

    this._prepareConnect();
    Connect.asyncRequest('POST', '/items/move_stack', this._defaultCallback, JSON.stringify({
      data: data
    }));

  };

  YAHOO.TRZCINKA.VCARD.GridModel.prototype.canMove = function(oldLocation, newLocation) {
    var linkLocations = this.getLinkLocations(oldLocation);
    var can = true;
    var that = this;
    linkLocations.each(function(linkLocation){
      if (!that._isLinkValid(newLocation, linkLocation)) {
        can = false;
      }
    });
    return can;
  }

  /**
     * Removes top item at given location.
     */
  YAHOO.TRZCINKA.VCARD.GridModel.prototype.removeItem = function(location) {
    LOGGER.log('removing item at ' + JSON.stringify(location), 'debug');

    var that = this;
    this.getLinkLocations(location).each(function (linkLocation) {
      that.unlinkItem(location, linkLocation);
    });

    var data = {
      location: location,
      gridId: this.getGridId()
    }
    var event = [Constants.ITEM_REMOVED_EVENT, data];
    this._handleEvent(event);
    this.gridModelEvent.fire(event);

    this._prepareConnect();
    Connect.asyncRequest('POST', '/items/destroy', this._defaultCallback, JSON.stringify({
      data: data
    }));
  };

  /**
     * Puts item with given order and given location on the top.
     */
  YAHOO.TRZCINKA.VCARD.GridModel.prototype.setTopItem = function(location, order) {
    LOGGER.log('setting top item at ' + JSON.stringify(location), 'debug');
    var data = {
      location: location,
      order: order,
      gridId: this.getGridId()
    }

    var event = [Constants.TOP_ITEM_SET_EVENT, data];
    this._handleEvent(event);
    this.gridModelEvent.fire(event);

    this._prepareConnect();
    Connect.asyncRequest('POST', '/items/set_top_item', this._defaultCallback, JSON.stringify({
      data: data
    }));
  };

  /**
     * Returns items at given location.
     */
  YAHOO.TRZCINKA.VCARD.GridModel.prototype.getItems = function(location) {
    LOGGER.log('getting items at ' + JSON.stringify(location), 'debug');
    return this.gridData.items.findAll(byLocation(location));
  };

  /**
     * Checks whether there is a stack at given location.
     */
  YAHOO.TRZCINKA.VCARD.GridModel.prototype.isStack = function(location) {
    LOGGER.log('checking if location is stack at ' + JSON.stringify(location), 'debug');
    return this.gridData.items.findAll(byLocation(location)).length > 1;
  };

  YAHOO.TRZCINKA.VCARD.GridModel.prototype.startLinking = function(location) {
    this._linkStartLocation = location;
    this._mode = Constants.MODE_LINKING;
    this.messageEvent.fire(I18n.t('editor.messages.click_link'));
  };
  
  YAHOO.TRZCINKA.VCARD.GridModel.prototype.cancelLinking = function() {
    this._linkStartLocation = null;
    this._mode = Constants.MODE_NORMAL;
    this.messageEvent.fire('');
  }

  YAHOO.TRZCINKA.VCARD.GridModel.prototype._isLinkValid = function(location1, location2) {
    if (this._linkExists(location1, location2)) {
      popup(I18n.t('editor.errors.already_linked'));
      return false;
    }
    return true;
  }

  YAHOO.TRZCINKA.VCARD.GridModel.prototype.finishLinking = function(location) {
    this.linkItem(this._linkStartLocation, location);
    this._linkStartLocation = null;
    this._mode = Constants.MODE_NORMAL;
    this.messageEvent.fire('');
  };

  YAHOO.TRZCINKA.VCARD.GridModel.prototype.linkItem = function(location1, location2) {
    if (!this._isLinkValid(location1, location2)) {
      return;
    }

    var data = {
      from: location1,
      to: location2,
      gridId: this.getGridId()
    }

    var event = [Constants.ITEMS_LINKED_EVENT, data];
    this._handleEvent(event);
    this.gridModelEvent.fire(event);

    this._prepareConnect();
    Connect.asyncRequest('POST', '/link/save', this._defaultCallback, JSON.stringify({
      data: data
    }));

    this._linkStartLocation = null;
    this._mode = Constants.MODE_NORMAL;

  };

  YAHOO.TRZCINKA.VCARD.GridModel.prototype.unlinkItem = function(location1, location2) {
    LOGGER.log('unlinking from ' + JSON.stringify(location1) + ' to ' + JSON.stringify(location2), 'debug');
    var data = {
      location1: location1,
      location2: location2,
      gridId: this.getGridId()
    }

    var event = [Constants.ITEMS_UNLINKED_EVENT, data];
    this._handleEvent(event);
    this.gridModelEvent.fire(event);

    this._prepareConnect();
    Connect.asyncRequest('POST', '/link/destroy', this._defaultCallback, JSON.stringify({
      data: data
    }));
  };

  YAHOO.TRZCINKA.VCARD.GridModel.prototype._linkExists = function(location1, location2) {
    return this.gridData.links.find(function(link) {
      return link.from.equals(location1) && link.to.equals(location2) ||
      link.from.equals(location2) && link.to.equals(location1);
    })
  };

  YAHOO.TRZCINKA.VCARD.GridModel.prototype.getMode = function() {
    return this._mode;
  };


  /**
   * Returns an array of locations that given location is linked to (both <tt>to</tt> and <tt>from</tt>).
   */
  YAHOO.TRZCINKA.VCARD.GridModel.prototype.getLinkLocations = function(location) {
    var result = [];
    this.gridData.links.each(function(link) {
      if (link.to.equals(location)) {
        result.push(link.from);
      } else if (link.from.equals(location)) {
        result.push(link.to);
      }
    });
    return result;
  };

  YAHOO.TRZCINKA.VCARD.GridModel.prototype.isNewSizeSafe = function(rows, columns) {
    if (this.gridData.items == undefined || rows >= this.gridData.rows && columns >= this.gridData.columns) {
      return true;
    }
    for (var i = 0; i < this.gridData.items.length; i++) {
      var item = this.gridData.items[i];
      if (item.location.row >= rows || item.location.column >= columns) {
        return false;
      }
    }
    return true;
  }

  YAHOO.TRZCINKA.VCARD.GridModel.prototype.changeDimensions = function(rows, columns) {
    LOGGER.log('changing grid dimensions to ' + rows + ' rows and ' + columns + ' columns', 'debug');
    var data = {
      rows: rows,
      columns: columns,
      gridId: this.getGridId()
    }

    var event = [Constants.DIMENSIONS_CHANGED_EVENT, data];

    this._prepareConnect();
    //TODO: shouldn't it be in grid controller?

    var that = this;
    Connect.asyncRequest(
      'POST',
      '/items/change_dimensions',
      {
        success: function() {
          that._handleEvent(event);        
        }
      },
    JSON.stringify({
      data: data
    }));

  this.gridModelEvent.fire(event);
}

YAHOO.TRZCINKA.VCARD.GridModel.prototype.isHighlighted = function(location) {
  return this.getTopItem(location).isHighlighted;
}

}());